/**
 * 
 */
package com.novartis.nims.trialsubmitter;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.channels.FileChannel;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.rsna.ctp.stdstages.anonymizer.AnonymizerStatus;

import com.novartis.nims.trialsubmitter.dicom.DicomCleaner;
import com.novartis.nims.util.ResourceAccess;

/**
 * @author Josh Snyder
 *
 */
class AnonymizerFilter extends TrialSubmitterStage {

	private Trial mTrial = null;
	private URI mBaseOutputLocation = null;
	private boolean mAbortOnUnspecifiedFileError;
	private boolean mCopyOnly = true;
	private String mVisitNameSeparator;
	private boolean mAllowBadPixelData;
	
	/**
	 * 
	 */
	AnonymizerFilter(TrialSubmitter ts, URI outputLocation) {
		super(ts);
		mBaseOutputLocation = outputLocation;
	}
		
	void setTrial(Trial trial) {
		mTrial = trial;
	}

	void setAbortOnUnspecifiedFileError(boolean value) {
		mAbortOnUnspecifiedFileError  = value;
	}
	
	void setCopyOnly(boolean copyOnly) {
		mCopyOnly = copyOnly;
	}

	void setVisitNameSeparator(String value) {
		mVisitNameSeparator = value;
	}

	void setAllowBadPixelData(boolean value) {
		mAllowBadPixelData = value;
	}
	
	void execute() throws AnonymizerFilterException {
		this.setMessage("Anonymizing images and copying to trial submission directory...");
		mLogger.info("Started cleaning images.");

		if (mTrial.isEmpty()) {
			throw new AnonymizerFilterException("Couldn't find any images in source directory. Try checking " +
				"the contents of the input subject directories.");	
		}
		
		// generate anonymization rules by converting pre-configured NIMS
		// specification into MIRC CTP Anonymizer rules
		Properties anonymizerRules = loadAnonymizerRules();
		
		//maintain a list of skipped images which must be removed from trial
		//after iteration for cleaning
		Set<Integer> imageKeysToRemove = new HashSet<Integer>();
		
		int numberOfImages = mTrial.keySet().size();
		int count = 1;
		for (Integer imageKey : mTrial.keySet()) {
			//abort if cancelled
			if (this.isCancelled()) {
				return;
			}
			
			URI imageReference = mTrial.getImageReference(imageKey);
			imageReference = mTrial.getBaseLocation().resolve(imageReference);
			
			SubmissionKey imageIdentifier = mTrial.getImageIdentifier(imageKey);

			mLogger.info("Cleaning " + imageReference.toString());
			
			//re-organize file directory structure based on image identifiers
			StringBuilder relativePath = new StringBuilder(60);
			relativePath.append(imageIdentifier.getTrialKey() + "/");
			relativePath.append(imageIdentifier.getSubjectKey() + "/");
			relativePath.append(imageIdentifier.getVisitKey() + mVisitNameSeparator);
			if (! imageIdentifier.getVisitNumber().isEmpty()) {
				relativePath.append(imageIdentifier.getVisitNumber());
			}
			relativePath.append(mVisitNameSeparator);
			relativePath.append(imageIdentifier.getVisitRepeatKey() + "/");
			relativePath.append(imageIdentifier.getScanKey() + "/");
			relativePath.append(imageIdentifier.getScanRepeatKey() + "/");
			
			//ensure file name is unique
			if (imageKey < 0) {
				//if imageKey is negative, prepend a 0, so file name doesn't start with a hyphen
				relativePath.append("0");
			}
			relativePath.append(imageKey + ".dcm");
			
			URI cleanedImageReference;
			try {
				cleanedImageReference = mBaseOutputLocation.resolve(
						new URI(null, null, relativePath.toString(), null));
			} catch (URISyntaxException e) {
				//programmer error. relativePath should be valid URI path component. any parts
				//based on user input should be validated at entry
				throw new IllegalArgumentException(relativePath.toString() + " isn't a valid path " +
						"argument for a new URI.", e);
			}
			
			
			File cleanedOutFile = new File(cleanedImageReference);
			if (cleanedOutFile.exists()) {
				throw new AnonymizerFilterException(cleanedImageReference + " already exists. Make sure " +
						"you don't modify the submission directory during operation.");
			}

			if (! cleanedOutFile.getParentFile().exists()) {
				if (! cleanedOutFile.getParentFile().mkdirs()) {
					throw new AnonymizerFilterException(cleanedOutFile.getParent() + " could not be created. " +
					"Make sure you can write to the submission directory.");
				}
			}
			
			// anonymize
			if (! mCopyOnly) {
				mLogger.info("Cleaning input (" + imageReference.getPath() + ") and saving to output (" +
						cleanedImageReference.getPath() + ").");
				
				try {
					anonymize(new File(imageReference), cleanedOutFile, anonymizerRules);
				} catch (AnonymizerFilterException e) {
					cleanedOutFile.delete();
					if (mAbortOnUnspecifiedFileError) {
						throw e;
					} else {
						mLogger.warn(e.getMessage());
						imageKeysToRemove.add(imageKey);
						continue;
					}
				}
			} else {
				mLogger.info("Copying input (" + imageReference.getPath() + ") to output (" +
						cleanedImageReference.getPath() + ").");
				
				try {
					this.copyFile(new File(imageReference), cleanedOutFile);
				} catch (IOException e) {
					cleanedOutFile.delete();
					throw new AnonymizerFilterException("Couldn't copy file to output directory: " +
							imageReference.getPath() + ". Make sure output directory wasn't changed " +
							"or removed.", e);
				}
			}
			
			mTrial.updateImageReference(imageKey, mBaseOutputLocation.relativize(cleanedImageReference));
			
			this.setProgress(100 * ++count / numberOfImages);
		}
		
		mTrial.setBaseLocation(mBaseOutputLocation);

		for (Integer imageKey : imageKeysToRemove) {
			mTrial.removeImage(imageKey);
		}
		
		mLogger.info("Done cleaning images.");
	}

	
	private Properties loadAnonymizerRules() throws AnonymizerFilterException {
		Properties props = new Properties();
		
		InputStream rulesIS = ResourceAccess.getResourceAsStream(ApplicationConfiguration.class, "generic.decs.xml");
		InputStream xslIS = ResourceAccess.getResourceAsStream(ApplicationConfiguration.class, "decs2mirc.xslt");

		StreamSource nimsRules = new StreamSource(rulesIS);
		StreamSource nimsToMircXsl = new StreamSource(xslIS);

		try {
			TransformerFactory tf = TransformerFactory.newInstance();
			ByteArrayOutputStream outStream = new ByteArrayOutputStream();
			StreamResult streamResult = new StreamResult(outStream);
			try {
				Transformer t = tf.newTransformer(nimsToMircXsl);
				t.transform(nimsRules, streamResult);
			} catch (TransformerException e) {
				throw new AnonymizerFilterException("Internal problem generating anonymization rules. " +
						"The application was probably configured and deployed incorrectly.", e);
			}

			try {
				props.load(new ByteArrayInputStream(outStream.toByteArray()));
			} catch (IOException e) {
				// reaching this means programmer error since ByteArrayInputStream
				// shouldn't cause an IOException on any operations
				throw new IllegalArgumentException(e);
			}
		} finally {
			try { rulesIS.close(); } catch (IOException ignore) {}
			try { xslIS.close(); } catch (IOException ignore) {}
		}
		
		return props;
	}


	private void anonymize(
			File inFile,
			File outFile,
			Properties anonymizerRules) throws AnonymizerFilterException {
		
		// perform the anonymization
		AnonymizerStatus result = DicomCleaner.anonymize(
				inFile, outFile,
				anonymizerRules, null,
				false, false, false);
		
		//if cleaning failed, try skipping pixel data
		if (! result.isOK() && mAllowBadPixelData) {
			result = DicomCleaner.anonymize(
					inFile, outFile,
					anonymizerRules, null,
					false, false, true);
		}
		
		// clean up after anonymize (creates a temp directory that it never deletes)
		File tempDir = new File(outFile.getParentFile().getParentFile(),
				"anonymizer-temp");
		if (tempDir.exists()) {
			tempDir.delete();
		}
		
		if (!result.isOK()) {
			throw new AnonymizerFilterException(
					inFile.getAbsolutePath() +
					" could not be anonymized. Try checking anonymization profile configuration," +
					" or fixing or discarding the input file.");
		}
	}

	
	private void copyFile(File in, File out) throws IOException	{
		FileChannel inChannel = new	FileInputStream(in).getChannel();
		FileChannel outChannel = new FileOutputStream(out).getChannel();
		try {
			inChannel.transferTo(0, inChannel.size(), outChannel);
		}
		finally {
			//Closing file channels will close corresponding stream objects as well.
			if (inChannel != null) inChannel.close();
			if (outChannel != null) outChannel.close();
		}
	}
}
